/**************************************************************************************************
	$Header: /pub/cvsroot/mydns/src/mydns/reply.c,v 1.23 2003/03/07 17:43:21 bboy Exp $
	reply.c: Routines to construct reply.

	Copyright (C) 2002  Don Moore <bboy@bboy.net>

	This program is free software; you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation; either version 2 of the License, or
	(at Your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program; if not, write to the Free Software
	Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
**************************************************************************************************/

#include "named.h"

#define	DEBUG_REPLY	1


/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
	REPLY_INIT
	Examines the question data, storing the name offsets (from DNS_HEADERSIZE) for compression.
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
void
reply_init(t)
	TASK *t;
{
	register u_char *c;											/* Current character in name */

	/* Examine question data, save labels found therein. The question data should begin with
		the name we've already parsed into t->qname.  I believe it is safe to assume that no
		compression will be possible in the question. */
	for (c = t->qname; *c; c++)
		if ((c == t->qname || *c == '.') && c[1])
			name_remember(t, (c == t->qname) ? c : (c+1),
					(((c == t->qname) ? c : (c+1)) - t->qname) + DNS_HEADERSIZE);
}
/*--- reply_init() ------------------------------------------------------------------------------*/


/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
	REPLY_ADD_ADDITIONAL
	Add ADDITIONAL for each item in the provided list.
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
static void
reply_add_additional(TASK *t, RRLIST *rrlist, datasection_t section)
{
	register RR *p;

	if (!rrlist)
		return;

	/* Examine each RR in the rrlist */
	for (p = rrlist->head; p; p = p->next)
		if (p->rrtype == DNS_RRTYPE_RR)
		{
			MYDNS_RR *rr = (MYDNS_RR *)p->rr;
			if (rr->type == DNS_T_NS || rr->type == DNS_T_MX)
			{
#if DEBUG_ENABLED && DEBUG_REPLY
				Debug("%s: resolving `%s' (A) for ADDITIONAL data", desctask(t), rr->data);
#endif
				(void)resolve(t, ADDITIONAL, DNS_T_A, rr->data, 0);
			}
			else if (rr->type == DNS_T_CNAME)
			{
#if DEBUG_ENABLED && DEBUG_REPLY
				Debug("%s: resolving `%s' (CNAME) for ADDITIONAL data", desctask(t), rr->data);
#endif
				/* Don't do this */
				(void)resolve(t, ADDITIONAL, DNS_T_CNAME, rr->data, 0);
			}
		}
}
/*--- reply_add_additional() --------------------------------------------------------------------*/


/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
	RDATA_ENLARGE
	Expands t->rdata by `size' bytes.  Returns a pointer to the destination.
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
static inline u_char *
rdata_enlarge(TASK *t, size_t size)
{
	t->rdlen += size;
	t->rdata = (!t->rdata) ? malloc(t->rdlen) : realloc(t->rdata, t->rdlen);
	if (!t->rdata)
		Err("malloc");
	return (t->rdata + t->rdlen - size);
}
/*--- rdata_enlarge() ---------------------------------------------------------------------------*/


/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
	REPLY_START_RR
	Begins an RR.  Appends to t->rdata all the header fields prior to rdlength.
	Returns the numeric offset of the start of this record within the reply.
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
static inline int
reply_start_rr(TASK *t, RR *r, u_char *name, dns_qtype_t type, u_int32_t ttl)
{
	u_char enc[DNS_MAXNAMELEN+1];
	u_char *dest;
	int enclen;

	if ((enclen = name_encode(t, enc, name, t->replylen + t->rdlen)) < 0)
		return (-1);

	r->length = enclen + SIZE16 + SIZE16 + SIZE32;

	dest = rdata_enlarge(t, r->length);

	r->offset = dest - t->rdata + DNS_HEADERSIZE + t->qdlen;

	DNS_PUT(dest, enc, enclen);
	DNS_PUT16(dest, type);
	DNS_PUT16(dest, DNS_C_IN);
	DNS_PUT32(dest, ttl);
	return (0);
}
/*--- reply_start_rr() --------------------------------------------------------------------------*/


/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
	REPLY_ADD_SOA
	Add a SOA record to the reply.
	Returns the numeric offset of the start of this record within the reply.
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
static inline int
reply_add_soa(TASK *t, RR *r)
{
	u_char *dest, ns[DNS_MAXNAMELEN+1], mbox[DNS_MAXNAMELEN+1];
	int size, nslen, mboxlen;
	MYDNS_SOA *soa = (MYDNS_SOA *)r->rr;

#if DEBUG_ENABLED && DEBUG_REPLY
	Debug("%s: REPLY_ADD: `%s' IN SOA", desctask(t), soa->origin);
#endif

	if (reply_start_rr(t, r, r->name, DNS_T_SOA, soa->ttl) < 0)
		return (-1);

	if ((nslen = name_encode(t, ns, soa->ns, CUROFFSET(t))) < 0)
		return (-1);
	if ((mboxlen = name_encode(t, mbox, soa->mbox, CUROFFSET(t) + nslen)) < 0)
		return (-1);

	size = nslen + mboxlen + (SIZE32 * 5);
	r->length += SIZE16 + size;

	dest = rdata_enlarge(t, SIZE16 + size);
	DNS_PUT16(dest, size);
	DNS_PUT(dest, ns, nslen);
	DNS_PUT(dest, mbox, mboxlen);
	DNS_PUT32(dest, soa->serial);
	DNS_PUT32(dest, soa->refresh);
	DNS_PUT32(dest, soa->retry);
	DNS_PUT32(dest, soa->expire);
	DNS_PUT32(dest, soa->minimum);
	return (0);
}
/*--- reply_add_soa() ---------------------------------------------------------------------------*/


/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
	REPLY_ADD_MX
	Returns the numeric offset of the start of this record within the reply.
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
static inline int
reply_add_mx(TASK *t, RR *r)
{
	u_char enc[DNS_MAXNAMELEN+1], *dest;
	int size, enclen;
	MYDNS_RR *rr = (MYDNS_RR *)r->rr;

#if DEBUG_ENABLED && DEBUG_REPLY
	Debug("%s: REPLY_ADD: `%s' IN MX `%u %s'", desctask(t), r->name, (u_int16_t)rr->aux, rr->data);
#endif

	if (reply_start_rr(t, r, r->name, DNS_T_MX, rr->ttl) < 0)
		return (-1);

	if ((enclen = name_encode(t, enc, rr->data, CUROFFSET(t) + SIZE16)) < 0)
		return (-1);

	size = SIZE16 + enclen;
	r->length += SIZE16 + size;

	dest = rdata_enlarge(t, SIZE16 + size);
	DNS_PUT16(dest, size);
	DNS_PUT16(dest, (u_int16_t)rr->aux);
	DNS_PUT(dest, enc, enclen);
	return (0);
}
/*--- reply_add_mx() ----------------------------------------------------------------------------*/


/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
	REPLY_ADD_PTR
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
static inline int
reply_add_ptr(TASK *t, RR *r)
{
	u_char enc[DNS_MAXNAMELEN+1], *dest;
	int size, enclen;
	MYDNS_PTR *ptr = (MYDNS_PTR *)r->rr;

#if DEBUG_ENABLED && DEBUG_REPLY
	Debug("%s: REPLY_ADD: `%s' IN PTR `%s'", desctask(t), r->name, ptr->name);
#endif

	if (reply_start_rr(t, r, r->name, DNS_T_PTR, ptr->ttl) < 0)
		return (-1);

	if ((enclen = name_encode(t, enc, ptr->name, CUROFFSET(t))) < 0)
		return (-1);

	size = enclen;
	r->length += SIZE16 + size;

	dest = rdata_enlarge(t, SIZE16 + size);
	DNS_PUT16(dest, size);
	DNS_PUT(dest, enc, enclen);
	return (0);
}
/*--- reply_add_ptr() ---------------------------------------------------------------------------*/


/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
	REPLY_ADD_TXT
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
static inline int
reply_add_txt(TASK *t, RR *r)
{
	u_char *dest;
	u_char size;
	size_t len;
	MYDNS_RR *rr = (MYDNS_RR *)r->rr;

#if DEBUG_ENABLED && DEBUG_REPLY
	Debug("%s: REPLY_ADD: `%s' IN TXT", desctask(t), r->name);
#endif
	len = strlen(rr->data);

	if (reply_start_rr(t, r, r->name, DNS_T_TXT, rr->ttl) < 0)
		return (-1);

	size = len + 1;
	r->length += SIZE16 + size;

	dest = rdata_enlarge(t, SIZE16 + size);
	DNS_PUT16(dest, size);
	*dest++ = len;
	memcpy(dest, rr->data, len);
	dest += len;
	return (0);
}
/*--- reply_add_txt() ---------------------------------------------------------------------------*/


/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
	REPLY_ADD_A
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
static inline int
reply_add_a(TASK *t, RR *r)
{
	u_char *dest;
	int size;
	MYDNS_RR *rr = (MYDNS_RR *)r->rr;
	struct in_addr addr;

	if (inet_pton(AF_INET, rr->data, (void *)&addr) > 0)
	{
#if DEBUG_ENABLED && DEBUG_REPLY
		Debug("%s: REPLY_ADD: `%s' IN A %s", desctask(t), r->name, inet_ntoa(addr));
#endif
		if (reply_start_rr(t, r, r->name, DNS_T_A, rr->ttl) < 0)
			return (-1);

		size = SIZE32;
		r->length += SIZE16 + size;

		dest = rdata_enlarge(t, SIZE16 + size);
		DNS_PUT16(dest, size);
		DNS_PUT32(dest, ntohl(addr.s_addr));
	}
	return (0);
}
/*--- reply_add_a() -----------------------------------------------------------------------------*/


/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
	REPLY_ADD_AAAA
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
static inline int
reply_add_aaaa(TASK *t, RR *r)
{
	u_char *dest;
	int size;
	MYDNS_RR *rr = (MYDNS_RR *)r->rr;
	u_int8_t addr[16];

#if DEBUG_ENABLED && DEBUG_REPLY
	Debug("%s: REPLY_ADD: `%s' IN AAAA %s", desctask(t), r->name, rr->data);
#endif

	if (inet_pton(AF_INET6, rr->data, (void *)&addr) > 0)
	{
		if (reply_start_rr(t, r, r->name, DNS_T_AAAA, rr->ttl) < 0)
			return (-1);

		size = sizeof(u_int8_t) * 16;
		r->length += SIZE16 + size;

		dest = rdata_enlarge(t, SIZE16 + size);
		DNS_PUT16(dest, size);
		memcpy(dest, &addr, size);
		dest += size;
	}
	return (0);
}
/*--- reply_add_aaaa() --------------------------------------------------------------------------*/


/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
	REPLY_ADD_GENERIC_RR
	Adds a generic resource record whose sole piece of data is a domain-name,
	or a 16-bit value plus a domain-name.
	Returns the numeric offset of the start of this record within the reply.
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
static inline int
reply_add_generic_rr(TASK *t, RR *r)
{
	u_char enc[DNS_MAXNAMELEN+1], *dest;
	int size, enclen;
	MYDNS_RR *rr = (MYDNS_RR *)r->rr;

#if DEBUG_ENABLED && DEBUG_REPLY
	Debug("%s: REPLY_ADD: `%s' IN %s `%s'", desctask(t), r->name, mydns_qtype_str(rr->type), rr->data);
#endif

	if (reply_start_rr(t, r, r->name, rr->type, rr->ttl) < 0)
		return (-1);

	if ((enclen = name_encode(t, enc, rr->data, CUROFFSET(t))) < 0)
		return (-1);

	size = enclen;
	r->length += SIZE16 + size;

	dest = rdata_enlarge(t, SIZE16 + size);
	DNS_PUT16(dest, size);
	DNS_PUT(dest, enc, enclen);
	return (0);
}
/*--- reply_add_generic_rr() --------------------------------------------------------------------*/


/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
	REPLY_PROCESS_RRLIST
	Adds each resource record found in `rrlist' to the reply.
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
static int
reply_process_rrlist(TASK *t, RRLIST *rrlist)
{
	register RR *r;

	if (!rrlist)
		return (0);

	for (r = rrlist->head; r; r = r->next)
	{
		switch (r->rrtype)
		{
			case DNS_RRTYPE_SOA:
				if (reply_add_soa(t, r) < 0)
					return (-1);
				break;

			case DNS_RRTYPE_RR:
				{
					MYDNS_RR *rr = (MYDNS_RR *)r->rr;
					switch (rr->type)
					{
						case DNS_T_CNAME:
							if (reply_add_generic_rr(t, r) < 0)
								return (-1);
							break;

						case DNS_T_MX:
							if (reply_add_mx(t, r) < 0)
								return (-1);
							break;

						case DNS_T_NS:
							if (reply_add_generic_rr(t, r) < 0)
								return (-1);
							break;

						case DNS_T_TXT:
							if (reply_add_txt(t, r) < 0)
								return (-1);
							break;

						case DNS_T_A:
							if (reply_add_a(t, r) < 0)
								return (-1);
							break;

						case DNS_T_AAAA:
							if (reply_add_aaaa(t, r) < 0)
								return (-1);
							break;

						case DNS_T_PTR:
							if (reply_add_generic_rr(t, r) < 0)
								return (-1);
							break;

						default:
							Warnx("%s: %s: %s", desctask(t), mydns_qtype_str(rr->type),
									_("unsupported query type"));
					}
				}
				break;

			case DNS_RRTYPE_PTR:
				if (reply_add_ptr(t, r) < 0)
					return (-1);
				break;
		}
	}
	return (0);
}
/*--- reply_process_rrlist() --------------------------------------------------------------------*/


/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
	TRUNCATE_RRLIST
	Returns new count of items in this list.
	The TC flag is _not_ set if data was truncated from the ADDITIONAL section.
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
static int
truncate_rrlist(TASK *t, off_t maxpkt, RRLIST *rrlist, datasection_t ds)
{
	register RR *rr;
	register int recs;

	/* Warn about truncated packets, but only if TCP is not enabled.  Most resolvers will try
		TCP if a UDP packet is truncated. */
	if (!tcp_enabled)
		Warnx("%s: %s", desctask(t), _("query truncated"));

	recs = rrlist->size;
	for (rr = rrlist->head; rr; rr = rr->next)
	{
		if (rr->offset + rr->length >= maxpkt)
		{
			recs--;
			if (ds != ADDITIONAL)
				t->hdr.tc = 1;
		}
		else
			t->rdlen += rr->length;
	}
	return (recs);
}
/*--- truncate_rrlist() -------------------------------------------------------------------------*/


/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
	REPLY_CHECK_TRUNCATION
	If this reply would be truncated, removes any RR's that won't fit and sets the truncation flag.
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
static void
reply_check_truncation(TASK *t, int *ancount, int *nscount, int *arcount)
{
	size_t maxpkt = (t->protocol == SOCK_STREAM ? DNS_MAXPACKETLEN_TCP : DNS_MAXPACKETLEN_UDP);
	size_t maxrd = maxpkt - (DNS_HEADERSIZE + t->qdlen);

	if (t->rdlen <= maxrd)
		return;

	/* Loop through an/ns/ar sections, truncating as necessary, and updating counts */
	t->rdlen = 0;
	*ancount = truncate_rrlist(t, maxpkt, &t->an, ANSWER);
	*nscount = truncate_rrlist(t, maxpkt, &t->ns, AUTHORITY);
	*arcount = truncate_rrlist(t, maxpkt, &t->ar, ADDITIONAL);
}
/*--- reply_check_truncation() ------------------------------------------------------------------*/


/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
	BUILD_REPLY
	Given a task, constructs the reply data.
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/
void
build_reply(TASK *t, int want_additional)
{
	u_char *dest;
	int ancount, nscount, arcount;

	if (t->an.addresses > 1)									/* Order multiple A/AAAA records */
		sort_addresses(t, &t->an);
	if (t->an.mxrecords > 1)									/* Order multiple MX records */
		sort_mx(t, &t->an);

	/* Add data to ADDITIONAL section */
	if (want_additional)
	{
		reply_add_additional(t, &t->an, ANSWER);
		reply_add_additional(t, &t->ns, AUTHORITY);
	}

	/* Build `rdata' containing resource records in ANSWER, AUTHORITY, and ADDITIONAL */
	t->replylen = DNS_HEADERSIZE + t->qdlen + t->rdlen;
	if (reply_process_rrlist(t, &t->an)
		 || reply_process_rrlist(t, &t->ns)
		 || reply_process_rrlist(t, &t->ar))
	{
		/* Empty RR lists */
		rrlist_free(&t->an);
		rrlist_free(&t->ns);
		rrlist_free(&t->ar);

		/* Make sure reply is empty */
		t->replylen = 0;
		t->rdlen = 0;
		if (t->rdata)
		  free(t->rdata);
	}

	ancount = t->an.size;
	nscount = t->ns.size;
	arcount = t->ar.size;

	/* Verify reply length */
	reply_check_truncation(t, &ancount, &nscount, &arcount);

	/* Construct the reply */
	t->replylen = DNS_HEADERSIZE + t->qdlen + t->rdlen;
	dest = t->reply = malloc(t->replylen);
	if (!t->reply)
		Err("malloc");

	DNS_PUT16(dest, t->id);										/* Query ID */
	DNS_PUT(dest, &t->hdr, SIZE16);							/* Header */
	DNS_PUT16(dest, t->qdcount);								/* QUESTION count */
	DNS_PUT16(dest, ancount);									/* ANSWER count */
	DNS_PUT16(dest, nscount);									/* AUTHORITY count */
	DNS_PUT16(dest, arcount);									/* ADDITIONAL count */
	if (t->qdlen && t->qd)
		DNS_PUT(dest, t->qd, t->qdlen);						/* Data for QUESTION section */
	DNS_PUT(dest, t->rdata, t->rdlen);						/* Resource record data */

#if DEBUG_ENABLED && DEBUG_REPLY
	Debug("%s: reply: len=%u qd=%d, an=%d, ns=%d, ar=%d", desctask(t),
			t->replylen, t->qdcount, ancount, nscount, arcount);
	/* escdata(t->reply, t->replylen); */
#endif
}
/*--- build_reply() -----------------------------------------------------------------------------*/

/* vi:set ts=3: */
/* NEED_PO */
